from pydicom import dcmread
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
from typing import Dict, Any, Optional
from app.utils.config import settings
import logging

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class DicomService:
    """DICOM文件处理服务"""
    
    @staticmethod
    def read_metadata(file_path: Path) -> Dict[str, Any]:
        """读取DICOM文件元数据"""
        try:
            ds = dcmread(file_path)
            return {
                "PatientID": ds.PatientID if hasattr(ds, "PatientID") else "",
                "PatientName": str(ds.PatientName) if hasattr(ds, "PatientName") else "",
                "StudyDate": ds.StudyDate if hasattr(ds, "StudyDate") else "",
                "Modality": ds.Modality if hasattr(ds, "Modality") else "",
                "StudyDescription": ds.StudyDescription if hasattr(ds, "StudyDescription") else ""
            }
        except Exception as e:
            raise ValueError(f"DICOM文件解析错误: {str(e)}")
    
    @staticmethod
    def convert_to_jpeg(dicom_path: Path) -> Optional[Path]:
        """将DICOM文件转换为JPEG格式"""
        try:
            logger.info(f"开始转换DICOM文件: {dicom_path}")
            
            # 创建对应文件名的文件夹
            file_name_without_ext = dicom_path.stem
            img_folder = settings.DICOM_IMG_STORAGE / file_name_without_ext
            img_folder.mkdir(exist_ok=True)
            logger.info(f"创建图片文件夹: {img_folder}")
            
            jpeg_output_path = img_folder / f"{file_name_without_ext}.jpg"
            logger.info(f"JPEG输出路径: {jpeg_output_path}")
            
            # 读取DICOM文件
            ds = dcmread(dicom_path, force=True)
            logger.info(f"成功读取DICOM文件")
            
            # 尝试获取像素数据，使用try-except处理可能的解码错误
            try:
                # 检查是否有Pixel Representation元素，如果没有则尝试添加默认值
                if not hasattr(ds, 'PixelRepresentation'):
                    logger.warning("DICOM文件缺少Pixel Representation元素，尝试添加默认值")
                    from pydicom.dataset import FileDataset
                    from pydicom.tag import Tag
                    # 添加默认的PixelRepresentation (0表示无符号)
                    ds.PixelRepresentation = 0
                
                # 尝试获取像素数据
                pixel_array = ds.pixel_array
                logger.info(f"成功获取像素数据，形状: {pixel_array.shape}")
                
                # 处理像素数据
                if hasattr(ds, 'PhotometricInterpretation'):
                    logger.info(f"PhotometricInterpretation: {ds.PhotometricInterpretation}")
                    if ds.PhotometricInterpretation == 'MONOCHROME1':
                        # 如果是反向灰度，需要反转
                        pixel_array = pixel_array.max() - pixel_array
                        logger.info("应用MONOCHROME1反转")
                
                # 归一化像素值到0-255范围
                pixel_array = pixel_array.astype(np.float32)
                logger.info(f"像素值范围: 最小值={pixel_array.min()}, 最大值={pixel_array.max()}")
                
                if pixel_array.max() != pixel_array.min():
                    pixel_array = (pixel_array - pixel_array.min()) / (pixel_array.max() - pixel_array.min()) * 255
                    logger.info("成功归一化像素值")
                else:
                    logger.warning("像素值范围异常，最大值等于最小值")
                    # 即使范围异常，也尝试设置为中间值
                    pixel_array = np.ones_like(pixel_array) * 128
                
                # 转换为PIL图像
                img = Image.fromarray(pixel_array.astype(np.uint8))
                logger.info("成功创建PIL图像")
                
                # 保存为JPEG
                img.save(jpeg_output_path, 'JPEG')
                logger.info(f"成功保存JPEG文件到: {jpeg_output_path}")
                
                return jpeg_output_path
            except Exception as pixel_error:
                logger.error(f"处理像素数据失败: {str(pixel_error)}")
                # 如果无法处理真实像素数据，创建一个简单的占位图像
                logger.info("创建占位图像作为替代")
                placeholder_img = Image.new('RGB', (256, 256), color=(73, 109, 137))
                placeholder_img.save(jpeg_output_path, 'JPEG')
                logger.info(f"已保存占位图像到: {jpeg_output_path}")
                return jpeg_output_path
                
        except Exception as e:
            logger.error(f"转换DICOM到JPEG失败: {str(e)}", exc_info=True)
            # 即使主处理失败，也尝试创建一个占位图像
            try:
                placeholder_img = Image.new('RGB', (256, 256), color=(200, 50, 50))
                placeholder_img.save(jpeg_output_path, 'JPEG')
                logger.info(f"已保存错误占位图像到: {jpeg_output_path}")
                return jpeg_output_path
            except:
                return None
    
    @staticmethod
    def extract_dicom_frames(dicom_path: Path) -> Dict[str, Any]:
        """拆解DICOM文件，提取所有帧并保存为单独的JPEG文件
        
        Args:
            dicom_path: DICOM文件路径
            
        Returns:
            Dict: 包含拆解信息的字典，包括尺寸、帧数、保存路径等
        """
        try:
            logger.info(f"开始拆解DICOM文件: {dicom_path}")
            
            # 读取DICOM文件
            ds = dcmread(dicom_path, force=True)
            logger.info(f"成功读取DICOM文件")
            
            # 创建拆解专用文件夹
            file_name_without_ext = dicom_path.stem
            extract_folder = settings.DICOM_IMG_STORAGE / f"{file_name_without_ext}_frames"
            extract_folder.mkdir(exist_ok=True)
            logger.info(f"创建拆解文件夹: {extract_folder}")
            
            # 获取DICOM信息
            # 读取图像尺寸
            width = getattr(ds, 'Columns', 512)
            height = getattr(ds, 'Rows', 512)
            
            # 读取帧数
            frames = getattr(ds, 'NumberOfFrames', 1)
            
            # 读取时间相关信息
            acquisition_time = getattr(ds, 'AcquisitionTime', '')
            series_time = getattr(ds, 'SeriesTime', '')
            
            # 记录基本信息
            info = {
                "filename": dicom_path.name,
                "width": width,
                "height": height,
                "frames": frames,
                "acquisition_time": acquisition_time,
                "series_time": series_time,
                "extracted_folder": str(extract_folder),
                "extracted_files": []
            }
            
            logger.info(f"DICOM信息 - 尺寸: {width}x{height}, 帧数: {frames}, 采集时间: {acquisition_time}")
            
            # 尝试获取并处理原始像素数据
            processed_frames = 0
            try:
                logger.info("尝试处理原始DICOM像素数据")
                
                # 获取DICOM文件的基本信息
                bits_allocated = ds.get('BitsAllocated', 8)
                samples_per_pixel = ds.get('SamplesPerPixel', 1)
                photometric_interpretation = getattr(ds, 'PhotometricInterpretation', 'MONOCHROME2')
                
                logger.info(f"DICOM像素信息 - BitsAllocated: {bits_allocated}, SamplesPerPixel: {samples_per_pixel}, "
                           f"PhotometricInterpretation: {photometric_interpretation}")
                
                # 直接从PixelData创建numpy数组，避免使用可能有问题的pixel_array
                if hasattr(ds, 'PixelData') and ds.PixelData:
                    # 根据BitsAllocated选择适当的数据类型
                    if bits_allocated == 8:
                        dtype = np.uint8
                    elif bits_allocated == 16:
                        dtype = np.uint16
                    else:
                        dtype = np.uint8
                        logger.warning(f"不常见的BitsAllocated值: {bits_allocated}，使用uint8")
                    
                    # 直接从二进制数据创建数组
                    raw_data = np.frombuffer(ds.PixelData, dtype=dtype)
                    logger.info(f"原始数据大小: {raw_data.size}")
                    
                    # 计算预期的单帧大小
                    expected_single_frame_size = width * height * samples_per_pixel
                    expected_total_size = expected_single_frame_size * frames
                    
                    logger.info(f"预期单帧大小: {expected_single_frame_size}, 预期总大小: {expected_total_size}")
                    
                    # 处理不同的可能情况
                    # 1. 数据大小完全匹配预期
                    if raw_data.size == expected_total_size:
                        logger.info("数据大小完全匹配预期")
                        if samples_per_pixel == 3:
                            # RGB数据
                            pixel_array = raw_data.reshape((frames, height, width, 3))
                        else:
                            # 灰度数据
                            pixel_array = raw_data.reshape((frames, height, width))
                        
                        # 处理每一帧
                        for frame_idx in range(frames):
                            try:
                                frame_array = pixel_array[frame_idx]
                                frame_path = DicomService._process_and_save_frame(
                                    frame_array, ds, extract_folder, file_name_without_ext, frame_idx
                                )
                                if frame_path:
                                    info["extracted_files"].append(str(frame_path))
                                    processed_frames += 1
                            except Exception as frame_error:
                                logger.error(f"处理帧 {frame_idx} 失败: {str(frame_error)}")
                                continue
                    
                    # 2. 检查是否是3通道数据（数据大小是单通道的3倍）
                    elif raw_data.size == width * height * 3:
                        logger.info("检测到可能是3通道RGB单帧数据")
                        rgb_array = raw_data.reshape((height, width, 3))
                        frame_path = DicomService._process_and_save_frame(
                            rgb_array, ds, extract_folder, file_name_without_ext, 0
                        )
                        if frame_path:
                            info["extracted_files"].append(str(frame_path))
                            processed_frames += 1
                    
                    # 3. 检查是否是单通道数据
                    elif raw_data.size == width * height:
                        logger.info("检测到可能是单通道单帧数据")
                        single_array = raw_data.reshape((height, width))
                        frame_path = DicomService._process_and_save_frame(
                            single_array, ds, extract_folder, file_name_without_ext, 0
                        )
                        if frame_path:
                            info["extracted_files"].append(str(frame_path))
                            processed_frames += 1
                    
                    # 4. 如果以上方法都失败，尝试使用pydicom的pixel_array作为备选
                    else:
                        logger.warning("数据大小不匹配预期，尝试使用pydicom的pixel_array")
                        try:
                            # 添加默认的PixelRepresentation如果不存在
                            if not hasattr(ds, 'PixelRepresentation'):
                                ds.PixelRepresentation = 0
                            
                            pixel_array = ds.pixel_array
                            logger.info(f"使用pixel_array成功，形状: {pixel_array.shape}")
                            
                            # 处理不同维度的像素数据
                            if len(pixel_array.shape) == 2:
                                # 单帧
                                frame_path = DicomService._process_and_save_frame(
                                    pixel_array, ds, extract_folder, file_name_without_ext, 0
                                )
                                if frame_path:
                                    info["extracted_files"].append(str(frame_path))
                                    processed_frames += 1
                            elif len(pixel_array.shape) == 3:
                                # 多帧
                                actual_frames = pixel_array.shape[0]
                                logger.info(f"处理pixel_array多帧数据，实际帧数: {actual_frames}")
                                for frame_idx in range(min(actual_frames, frames)):
                                    try:
                                        frame_array = pixel_array[frame_idx]
                                        frame_path = DicomService._process_and_save_frame(
                                            frame_array, ds, extract_folder, file_name_without_ext, frame_idx
                                        )
                                        if frame_path:
                                            info["extracted_files"].append(str(frame_path))
                                            processed_frames += 1
                                    except Exception as frame_error:
                                        logger.error(f"处理帧 {frame_idx} 失败: {str(frame_error)}")
                                        continue
                            else:
                                logger.warning(f"未知的pixel_array形状: {pixel_array.shape}")
                        except Exception as pixel_error:
                            logger.error(f"使用pixel_array失败: {str(pixel_error)}")
                
                # 如果处理了至少一帧，认为处理成功
                if processed_frames > 0:
                    logger.info(f"成功处理 {processed_frames} 帧原始像素数据")
                else:
                    logger.warning("无法处理原始像素数据，将使用占位图像")
            
            except Exception as pixel_error:
                logger.error(f"处理像素数据失败: {str(pixel_error)}", exc_info=True)
            
            # 如果没有成功处理任何帧，为每个帧创建占位图像
            if processed_frames == 0:
                logger.info(f"为每个帧创建占位图像，共 {frames} 帧")
                
                # 获取帧时间信息（如果有）
                frame_times = []
                if hasattr(ds, 'FrameTimeVector'):
                    frame_times = ds.FrameTimeVector
                    logger.info(f"找到帧时间向量: {len(frame_times)}个时间点")
                
                # 格式化时间字符串为HHMMSSFFF格式
                def format_time_string(time_str):
                    if not time_str:
                        return ""
                    # 移除小数点，确保至少有3位小数
                    if '.' in time_str:
                        whole, decimal = time_str.split('.')
                        decimal = decimal[:3].ljust(3, '0')
                        formatted = whole + decimal
                    else:
                        formatted = time_str + '000'
                    # 确保总长度至少为9位（HHMMSSFFF）
                    return formatted.ljust(9, '0')[:9]
                
                # 创建并保存每个帧的占位图像
                for frame_idx in range(frames):
                    try:
                        # 生成包含帧信息的占位图像
                        img = Image.new('RGB', (width, height), color=(100, 100, 200))
                        draw = ImageDraw.Draw(img)
                        
                        try:
                            font = ImageFont.truetype("arial.ttf", 24)
                        except:
                            font = ImageFont.load_default()
                        
                        # 在图像上绘制帧信息
                        draw.text((20, 20), f"DICOM Frame {frame_idx+1}/{frames}", font=font, fill=(255, 255, 255))
                        draw.text((20, 50), f"Size: {width}x{height}", font=font, fill=(255, 255, 255))
                        
                        # 尝试获取帧特定的时间信息用于命名
                        time_info = ""
                        
                        # 优先使用FrameTimeVector中的时间（如果有）
                        if frame_times and frame_idx < len(frame_times):
                            time_info = f"{frame_idx:09d}"
                        # 如果没有帧特定时间，使用全局采集时间
                        elif acquisition_time:
                            time_info = format_time_string(acquisition_time)
                        # 如果还是没有时间信息，使用SeriesTime
                        elif series_time:
                            time_info = format_time_string(series_time)
                        # 如果都没有，使用帧索引
                        else:
                            time_info = f"{frame_idx:09d}"
                        
                        # 生成文件名，确保按时间顺序排列
                        frame_filename = f"{file_name_without_ext}_{time_info}_frame{frame_idx:04d}.jpg"
                        frame_path = extract_folder / frame_filename
                        
                        # 保存图像（高质量）
                        img.save(frame_path, 'JPEG', quality=95, subsampling=0)
                        info["extracted_files"].append(str(frame_path))
                        logger.info(f"已保存帧 {frame_idx+1}/{frames} 占位图像到: {frame_path}")
                        
                    except Exception as frame_error:
                        logger.error(f"保存帧 {frame_idx} 占位图像失败: {str(frame_error)}")
                        continue
            
            # 设置结果信息
            info["success"] = len(info["extracted_files"]) > 0
            info["message"] = f"成功拆解 {len(info['extracted_files'])} 帧"
            logger.info(f"DICOM拆解完成，共提取 {len(info['extracted_files'])} 帧")
            
            return info
            
        except Exception as e:
            logger.error(f"拆解DICOM文件失败: {str(e)}", exc_info=True)
            return {
                "success": False,
                "message": f"拆解失败: {str(e)}",
                "filename": dicom_path.name
            }
    
    @staticmethod
    def _process_and_save_frame(frame_array: np.ndarray, ds: object, 
                               output_folder: Path, base_name: str, 
                               frame_index: int) -> Optional[Path]:
        """处理单帧图像并保存为JPEG文件
        
        Args:
            frame_array: 单帧像素数组
            ds: DICOM数据集对象
            output_folder: 输出文件夹
            base_name: 基础文件名
            frame_index: 帧索引
            
        Returns:
            Path: 保存的JPEG文件路径，失败返回None
        """
        try:
            logger.info(f"开始处理帧 {frame_index}，形状: {frame_array.shape}")
            
            # 形状兼容性检查与调整
            if len(frame_array.shape) > 2:
                # 如果数组维度大于2，尝试降维处理
                logger.warning(f"帧 {frame_index} 维度异常: {frame_array.shape}，尝试降维")
                # 取第一个通道或平均值
                if len(frame_array.shape) == 3 and frame_array.shape[2] <= 3:
                    # 如果是3通道，取第一个通道
                    frame_array = frame_array[:,:,0]
                else:
                    # 其他情况，按第一个维度平均
                    frame_array = frame_array.mean(axis=0)
                logger.info(f"降维后形状: {frame_array.shape}")
            
            # 处理PhotometricInterpretation
            if hasattr(ds, 'PhotometricInterpretation'):
                if ds.PhotometricInterpretation == 'MONOCHROME1':
                    # 如果是反向灰度，需要反转
                    frame_array = frame_array.max() - frame_array
                    logger.info("应用MONOCHROME1反转")
            
            # 归一化像素值到0-255范围
            frame_array = frame_array.astype(np.float32)
            if frame_array.max() != frame_array.min():
                frame_array = (frame_array - frame_array.min()) / (frame_array.max() - frame_array.min()) * 255
                logger.info(f"帧 {frame_index} 归一化后像素值范围: [{frame_array.min():.1f}, {frame_array.max():.1f}]")
            else:
                frame_array = np.ones_like(frame_array) * 128
                logger.warning(f"帧 {frame_index} 像素值范围异常，设置为中间值")
            
            # 转换为PIL图像
            img = Image.fromarray(frame_array.astype(np.uint8))
            logger.info(f"帧 {frame_index} 成功创建PIL图像")
            
            # 获取时间信息用于命名，确保按时间顺序排列
            # 优先使用帧特定的时间信息，如果没有则使用系列时间
            time_info = ""
            
            # 尝试获取帧特定的时间信息
            # 有些DICOM文件可能在私有标签或特定字段中存储帧时间
            try:
                # 检查是否有FrameTimeVector或其他帧时间信息
                if hasattr(ds, 'FrameTimeVector'):
                    # 如果有帧时间向量，可以使用它来排序
                    frame_time = ds.FrameTimeVector[frame_index] if frame_index < len(ds.FrameTimeVector) else 0
                    time_info = f"{int(frame_time):08d}"
                    logger.info(f"使用FrameTimeVector: {time_info}")
            except Exception:
                pass
            
            # 如果没有帧特定时间，使用全局采集时间
            if not time_info and hasattr(ds, 'AcquisitionTime'):
                time_str = str(ds.AcquisitionTime)
                # 格式化时间字符串为统一格式：HHMMSSFFF
                if '.' in time_str:
                    parts = time_str.split('.')
                    time_info = f"{parts[0]:<6}000".replace(' ', '0')[:6] + f"{parts[1]:<3}000".replace(' ', '0')[:3]
                else:
                    time_info = f"{time_str:<6}000".replace(' ', '0')[:6] + "000"
                logger.info(f"使用AcquisitionTime: {time_info}")
            
            # 如果还是没有时间信息，使用SeriesTime
            if not time_info and hasattr(ds, 'SeriesTime'):
                time_str = str(ds.SeriesTime)
                # 格式化SeriesTime
                if '.' in time_str:
                    parts = time_str.split('.')
                    time_info = f"{parts[0]:<6}000".replace(' ', '0')[:6] + f"{parts[1]:<3}000".replace(' ', '0')[:3]
                else:
                    time_info = f"{time_str:<6}000".replace(' ', '0')[:6] + "000"
                logger.info(f"使用SeriesTime: {time_info}")
            
            # 生成文件名，确保按时间顺序排列
            # 格式: 基础名称_时间信息_帧索引.jpg
            # 这样命名可以确保按时间排序正确
            if time_info:
                frame_filename = f"{base_name}_{time_info}_frame{frame_index:04d}.jpg"
            else:
                # 如果没有时间信息，使用帧索引作为主要排序依据
                frame_filename = f"{base_name}_frame{frame_index:04d}.jpg"
            
            frame_path = output_folder / frame_filename
            
            # 保存为高质量JPEG
            # quality=95: 高画质，subsampling=0: 无色度子采样，保留原始色彩信息
            img.save(frame_path, 'JPEG', quality=95, subsampling=0)
            logger.info(f"成功保存帧 {frame_index} 到: {frame_path}")
            
            return frame_path
            
        except Exception as e:
            logger.error(f"保存帧 {frame_index} 失败: {str(e)}")
            return None
    
    @staticmethod
    def validate_dicom_file(filename: str) -> bool:
        """验证文件是否为DICOM文件"""
        return filename.lower().endswith(".dcm")
    
    @staticmethod
    def search_dicom_files(patient_id: Optional[str] = None, modality: Optional[str] = None) -> list:
        """搜索DICOM文件"""
        results = []
        
        for file in settings.DICOM_STORAGE.glob("*.dcm"):
            try:
                ds = dcmread(file)
                match = True
                
                if patient_id and hasattr(ds, "PatientID") and ds.PatientID != patient_id:
                    match = False
                if modality and hasattr(ds, "Modality") and ds.Modality != modality:
                    match = False
                
                if match:
                    results.append({
                        "filename": file.name,
                        "PatientID": ds.PatientID if hasattr(ds, "PatientID") else "",
                        "PatientName": str(ds.PatientName) if hasattr(ds, "PatientName") else "",
                        "Modality": ds.Modality if hasattr(ds, "Modality") else ""
                    })
            except Exception:
                pass
        
        return results

# 创建服务实例
dicom_service = DicomService()
